#
# Copyright 2012 ibiblio
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0.txt
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from threading import Thread
import logging
import sys
import time

import pika
pika.adapters.select_connection.POLLER_TYPE = 'epoll'
from pika.adapters import SelectConnection

import seedbank.messaging.log_helper as log_helper
import seedbank.config.config_helper as config_helper

__all__ = ['RabbitMQConnector', 'RabbitMQConnectorException',
           'DELIVERY_TRANSIENT', 'DELIVERY_PERSISTENT',
           'DEFAULT_CONTENT_TYPE', 'BasicMessageHandler']

DELIVERY_TRANSIENT = 1
DELIVERY_PERSISTENT = 2

CONTENT_TYPE_TEXT = 'text/plain'
CONTENT_TYPE_BINARY = 'application/octet-stream'
DEFAULT_CONTENT_TYPE = CONTENT_TYPE_BINARY

def enable_pika_debug_log():
    logger = logging.getLogger('pika')
    handler = logging.StreamHandler(sys.stdout)
    formatter = logging.Formatter(log_helper._LOG_FORMAT_DEBUG)
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    logger.setLevel(logging.DEBUG)

def disable_pika_debug_log():
    logger = logging.getLogger('pika')
    logger.setLevel(logging.WARN)
    logger.addHandler(pika.log.NullHandler())

class RabbitMQConnectorException(Exception): pass

class MessageHandler(object):
    def handle(self, channel, method, header, body):
        pass

class BasicMessageHandler(MessageHandler):
    def handle(self, channel, method, header, body):
        channel.basic_ack(delivery_tag=method.delivery_tag)

class PrintingMessageHandler(BasicMessageHandler):
    def handle(self, channel, method, header, body):
        print 'Basic.Deliver %s delivery-tag %i: %s' % (
                  header.content_type,
                  method.delivery_tag,
                  body)
        print 'header'
        print header
        BasicMessageHandler.handle(self, channel, method, header, body)

class RabbitMQConnector(Thread):
    """
    Abstract class for managing communication with RabbitMQ message
    queues.
    """
    def __init__(self, **kwargs):
        Thread.__init__(self)
        self._config = kwargs.get('config', None)
        self._queue_name = kwargs.get('queue_name', None)
        self._routing_key = kwargs.get('routing_key', None)
        self._verbose = bool(kwargs.get('verbose', False))
        self._log = log_helper.get_logger(self.__class__.__name__)
        self._exchange = None

        # These are set during the connection process
        self._conn = None
        self._channel = None
        self._ready = False
        self._closed = False

    def run(self):
        """
        Called by Thread.start()
        """
        self._set_params_from_config(self._config)
        # Initiate connection
        self._conn = self._get_rabbitmq_connection(self._config)

        try:
            if self._verbose:
                self._log.info('Calling pika ioloop.start')
            self._conn.ioloop.start()
        except KeyboardInterrupt:
            self._log.info('Closing RabbitMQ connection')
            self._conn.close()
        except TypeError, e:
            self._log.error('TypeError in RabbitMQConnector::run (' + str(e) + ')')
            # Could be a pika bug -- TypeError: 'NoneType' object is unsubscriptable
            # pika/adapters/base_connection.py", line 106
            pass

        self._log.info('RabbitMQConnector thread terminating')

    def _set_params_from_config(self, config):
        self._exchange = config.get(config_helper.MQ_SECTION, 'exchange')

    def connected(self):
        return (self._conn is not None) and self._ready

    def _set_ready(self):
        if self._verbose:
            self._log.info('Connection is ready')
        self._ready = True

    def stop(self):
        try:
            self._stop()
        except KeyboardInterrupt:
            pass

    def _stop(self):
        self._log.info('Stopping RabbitMQConnector')
        if self._connection_is_open():
            self._conn.close()
            self._ready = False
            count = 0
            give_up = 50
            while not self._closed and count < give_up:
                time.sleep(.2)
                count += 1
        else:
            self._conn.ioloop.stop()

    def _connection_is_open(self):
        if not self._conn:
            return False
        return self._conn.is_open and not (self._conn.is_closing or self._conn.is_closed)

    def _get_rabbitmq_connection(self, config):
        self._preflight_check()
        params = self._get_connection_params(config)
        self._log.info('Connecting to RabbitMQ on %s:%s' % (params.host, params.port))
        if self._verbose:
            self._log.info('Connecting as %s to vhost %s' % (params.credentials.username, params.virtual_host))

        conn = SelectConnection(params, self.on_connected)
        conn.add_on_close_callback(self.on_closed)

        if self._verbose:
            conn.add_backpressure_callback(self.on_backpressure)
        return conn

    def _preflight_check(self):
        """
        Make sure the required settings are present before establishing
        a RabbitMQ connection.  The consumer and publisher classes override
        this method for additional checks.
        """
        if not self._exchange:
            raise ValueError('Missing exchange name')

    def _get_connection_params(self, config):
        mq_host = config.get(config_helper.MQ_SECTION, 'host')
        mq_port = config.getint(config_helper.MQ_SECTION, 'port')
        mq_user = config.get(config_helper.MQ_SECTION, 'user')
        mq_pass = config.get(config_helper.MQ_SECTION, 'pass')
        mq_vhost = config.get(config_helper.MQ_SECTION, 'vhost')

        credentials = pika.PlainCredentials(mq_user, mq_pass)
        params = pika.ConnectionParameters(credentials=credentials,
                                           host=mq_host, port=mq_port,
                                           virtual_host=mq_vhost)
        return params

    def on_connected(self, connection):
        """
        Callback triggered by pika establishing a connection to rabbitmq
        """
        self._log.info('Connection opened')
        connection.channel(self.on_channel_open)

    def on_channel_open(self, new_channel):
        self._log.info('Channel opened (exchange: %s)' % (self._exchange))
        self._channel = new_channel
        self._on_channel_open_stage2()

    def _on_channel_open_stage2(self):
        """
        Consumer and publisher classes must override this.
        """
        raise NotImplementedError()

    def _declare_queue(self, callback):
        """
        General function to declare a queue.  channel.queue_declare() requires
        a callback function, which should be passed in as a param here.
        """
        if not callback:
            raise RabbitMQConnectorException('Missing callback function')
        if self._verbose:
            self._log.info('Calling queue_declare (queue: %s)' % (self._queue_name))
        self._channel.queue_declare(queue=self._queue_name,
                                    durable=False,
                                    exclusive=False,
                                    auto_delete=False,
                                    callback=callback
                                    )

    def on_closed(self, frame):
        self._log.info('Connection closed')
        self._conn.ioloop.stop()
        self._closed =  True

    def on_backpressure(self, frame):
        self._log.info('Got tcp backpressure from RabbitMQ')

    def _print_state(self):
        if not self._conn:
            return
        print '************ self._conn.is_open is ' + str(self._conn.is_open)
        print '************ self._conn.is_closing is ' + str(self._conn.is_closing)
        print '************ self._conn.is_closed is ' + str(self._conn.is_closed)
